/*
 * WorxResponse.java
 * 
 * @author david
 */
package org.puresilk.android.net;

import java.io.IOException;
import java.io.StringReader;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import org.apache.http.util.ByteArrayBuffer;
import org.json.JSONException;
import org.json.JSONObject;
import org.puresilk.android.util.Logger;
import org.puresilk.android.xml.XMLParserDelegate;
import org.puresilk.android.xml.XMLParserException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import android.os.Handler;


/**
 * Worx web response processor.
 * 
 * The default will do some generalized
 * checking of the response and store the
 * results of the call into a string.  
 * 
 * Use preProcess() and postProcess()
 * to develop specialized parsers for 
 * JSON, XML. etc.
 * 
 */
public class HttpResponse implements HttpResponseDelegate {
	private static final Logger logger = Logger.getInstance("WorxResponse");
	
	// request / response type constants (default = String)
	public static final int DEFAULT_RESPONSE_TYPE = 0;
	public static final int JSON_RESPONSE_TYPE = 1;
	public static final int XML_RESPONSE_TYPE = 2;
	public static final int BINARY_RESPONSE_TYPE = 3;
	
	// status flags
	public static final int JSON_PARSING_ERROR = -4;
	public static final int PARSING_ERROR = -3;
	public static final int READING_ERROR = -1;
	public static final int NEW = 0;
	public static final int CONNECTED = 1;
	public static final int READING_DATA = 2;
	public static final int PARSING_DATA= 3;
	public static final int DATA_READY = 5;
	public static final int WRITE_UPDATE = 6;
	public static final int PROGRESS_UPDATE = 99;
	
	//--------------------------------------
	// process status / result codes
	//--------------------------------------
	private int resultCode = 0;
	private String resultMsg = null;
	
	// raw text from stream
	private ByteArrayBuffer result = null;
	
	// parsed HTTP data
	private String _httpResponseCode = null;
	private String _httpResponseMessage = null;
	private int _httpContentLength = -1;
	private Map<String,List<String>> _httpHeaders = null;
	
	// registered Handlers for callback messages
	private Vector<Handler> callers = new Vector<Handler>();
	
	// built in support for JSON
	private JSONObject json = null;

	// attributes
	private String strResponse = null;
	
	private int _type = DEFAULT_RESPONSE_TYPE;
	
	// -- constructors
	/**
	 * Default constructor
	 */
	public HttpResponse() {}
	
	/**
	 * Constructor w/Response type
	 * indicator.
	 * 
	 * @param rType
	 */
	public HttpResponse(int rType) {
		_type = rType;
	}
	
	//-----------------------------
	// API
	//-----------------------------
	
	/**
	 * check and see if the response is ready.
	 * 
	 * @return
	 */
	public synchronized boolean ready() {
		return resultCode == DATA_READY;
	}
		
	/**
	 * Generic return.
	 * 
	 * @return
	 */
	public synchronized Object getResults() {
			return !ready() ? null : _type == JSON_RESPONSE_TYPE ? json : result.toString();
	}
	
	/**
	 * Return results as JSON object
	 * 
	 * @return
	 */
	public synchronized JSONObject getJSONResults() {
		return ready() ? json : null;
	}
	
	/**
	 * Check the internal result code.
	 * @return
	 */
	public int getResultCode(){ return resultCode; }
	
	/**
	 * Check the internal results message.
	 * @return
	 */
	public String getResultMsg() { return resultMsg; }

	/**
	 * parse the results as XML and pass
	 * events to delegate.
	 * 
	 * @param dlgt
	 */	
	public synchronized void parse(XMLParserDelegate dlgt) throws XMLParserException {

		if (!ready())
			throw new XMLParserException("!! Data not ready");
		try {
			// create parser tools
			XmlPullParserFactory parserCreator = XmlPullParserFactory.newInstance();
			XmlPullParser parser = parserCreator.newPullParser();
			
			// need a Reader for the data
			StringReader src = new StringReader(result.toString());
			
			// set reader into parser
			parser.setInput(src);
			
			logger.debug("~~ Parsing XML...");
			
			dlgt.onStartDoc();
			int parserEvent = parser.getEventType();
			while (parserEvent != XmlPullParser.END_DOCUMENT) {
			    switch(parserEvent) {
			        case XmlPullParser.START_TAG:
			        	// mapping for attrs
						Map<String,String> attr = new HashMap<String,String>();
						
						// extract data from parser
			            String tag = parser.getName();
			            
			            int cnt = parser.getAttributeCount();
			            for(int i=0;i<cnt;i++)
			            	attr.put(parser.getAttributeName(i), parser.getAttributeValue(i));
			    		
			            // alert delegate 
			            dlgt.onStartTag(tag,attr);
			            break;
			        case XmlPullParser.TEXT:
			        	// pass thru to delegate
			        	dlgt.onSetText(parser.getText());
			        	break;
			        case XmlPullParser.END_TAG:
			        	// alert...
			        	dlgt.onEndTag(parser.getName());
			        }
			    
			    // get next event
			    parserEvent = parser.next();
			}
			// all done 
			dlgt.onEndDoc();
			
			logger.debug("XML Parse Done...");
		}
		catch(IOException ix){
			logger.error("IO Error when parsing",ix);
			dlgt.onRaiseError(ix);
		}
		catch(XmlPullParserException xm) {
			logger.error("XML Parse Error", xm);
			dlgt.onRaiseError(xm);
		}
	}
	
	//-------------------------------
	// http response field accessors
	//-------------------------------
	/**
	 * Retrieve the HTTP Response line
	 */
	public String getHttpResponse() {
		return String.format("%s - %s", _httpResponseCode,_httpResponseMessage);
	}

	/**
	 * Retrieve the HTTP result code (200, 404, 500, etc)
	 * @return
	 */
	public String getHttpResponseCode() {
		return _httpResponseCode;
	}

	/**
	 * Retrieve the HTTP result code message
	 * (OK, Not Found, Server Error, etc)
	 * @return
	 */
	public String getHttpResponseMessage() {
		return _httpResponseMessage;
	}
	
	/**
	 * Retrieve the whole set of HTTP headers.
	 * @return
	 */
	public Map<String, List<String>> getHeaders() {
		return _httpHeaders;
	}

	/**
	 * Retrieve the requested header value.
	 * @param nm
	 * @return
	 */
	public List<String> getHeader(String nm) {
		return _httpHeaders.get(nm);
	}

	/**
	 * Get expected content length.
	 * 
	 * @return
	 */
	public int getContentLength() { 
		return _httpContentLength; 
	}
	
	//--------------------
	// response retrieval
	//--------------------
	/**
	 * Get string (HTML) response type
	 * 
	 * @return
	 */
	public String getStringResponse() {
		if (strResponse == null) 
			strResponse = result.toByteArray().toString();
		
		return strResponse;
	}
	
	/**
	 * Retrieve the result data (after
	 * headers strip).	
	 * @return
	 */
	public byte [] getData() {
		return result.toByteArray();
	}

	/**
	 * Utility alias for getData();
	 *  
	 * @return
	 */
	public byte [] getBytes(){
		return getData();
	}
	
	/**
	 * Retrieve response type.
	 * @return
	 */
	public int getType() {
		return _type;
	}

	/**
	 * Set response type.
	 * @param _type
	 */
	public void setType(int _type) {
		this._type = _type;
	}
	
	//--------------------------------------------
	// Set up for the caller(s) of the request to 
	// get the progress and completion messages.
	//--------------------------------------------
	/** 
	 * Register to get progress info updates
	 * 
	 * @param hc
	 */
	public synchronized void registerCaller(Handler handler) {
		// if already complete, call go straight 
		// to the results
		
		if ((resultCode >= NEW) && (resultCode < DATA_READY))
			callers.add(handler);
		else
			handler.sendMessage(handler.obtainMessage(resultCode, this));
	}
	
	/**
	 * Sends progress update messages UI handlers
	 * 
	 * @param msg
	 */
	private synchronized void updateCallers(String msg){
		Iterator<Handler> itr = callers.iterator();
		while (itr.hasNext()){
			Handler hc = itr.next();
			hc.sendMessage(hc.obtainMessage(PROGRESS_UPDATE,msg));
		}		
	}
	
	/**
	 * Formats callback msg for progress info.
	 */
	private synchronized void updateCallers(int bRead) {
		String msg = Integer.toString(bRead) + " bytes read.";
		updateCallers(msg);
	}
	
	/**
	 * notify of mpf progrss
	 */
	private synchronized void requestUpdate(int bytesWritten) {
		String msg = Integer.toString(bytesWritten);
		Iterator<Handler> itr = callers.iterator();
		while (itr.hasNext()){
			Handler hc = itr.next();
			hc.sendMessage(hc.obtainMessage(WRITE_UPDATE,msg));
		}
	}
	
	/**
	 * Call back mechanism to alert
	 * UI handlers on state change.
	 */
	private void notifyCallers() {
		Iterator<Handler> itr = callers.iterator();
		while (itr.hasNext()){
			Handler hc = itr.next();
			hc.sendMessage(hc.obtainMessage(resultCode,this));
		}
	}
	
	/**
	 * Notify callers that an error has happened.
	 * (Note:  onResponseReady will not be called)
	 */
	private synchronized void notifyCallers(int errType, HttpException ex) {
		resultCode = errType;
		
		Iterator<Handler> itr = callers.iterator();
		while (itr.hasNext()){
			Handler hc = itr.next();
			hc.sendMessage(hc.obtainMessage(resultCode,ex));
		}
	}
	
	private void updateStatus(int newState){
		logger.debug("Updating Status:" + String.valueOf(newState));
		resultCode = newState;
		notifyCallers();
	}
	
	//--------------------------------
	//  Delegate methods
	//--------------------------------
	/**
	 * The server connection is made
	 * before posting any data
	 */
	public void onReadyToConnect() {
		updateStatus( NEW );
		logger.info("Initializing");
		updateCallers("Initializing");
		
		// (re)initialze response 
		_httpContentLength = -1;
		_httpResponseCode = null;
		_httpResponseMessage = null;
		_httpHeaders = null;
		
		resultCode = 0;
		resultMsg = null;
		
		result = null;
	}
	
	/**
	 * The server connection is made
	 * before posting any data
	 */
	public void onReadyToPost() {
		updateStatus( CONNECTED );
		logger.info("Connection made");
	}
	
	/**
	 * Sent data, ready to read
	 * the response from server.
	 */
	public void onReadyToRead(HttpURLConnection uc) {
		try{
			// (re)set HTTP Response Fields
			_httpContentLength = uc.getContentLength();
			_httpResponseCode  = String.valueOf(uc.getResponseCode());
			_httpResponseMessage = uc.getResponseMessage();
			
			// capture headers
			_httpHeaders = uc.getHeaderFields();
			
			// debug report
			logger.debug("Http Results: " + getHttpResponse() );
			
			// build new input buffer
			result = new ByteArrayBuffer(16);
			
			// feedback
			updateStatus( READING_DATA );
			
		}
		catch(Exception ex){
			// report and pass it on
			logger.error("Error getting header info",ex);
			updateStatus(READING_ERROR);      
		}
	}
	
	/**
	 * Reporting <i>sz</i> bytes of data 
	 * have been read.
	 */
	
	//method for updating on writing outputstream
	public void onDataWrite(int bytesWritten) {
		if (bytesWritten > 0)
			requestUpdate(bytesWritten);
	}
	
	public void onDataRead(byte[] data, int sz) {
		// slurping...
		if (sz > 0) {
			result.append(data,0,sz);
			updateCallers(sz);
		}
	}

	/**
	 * An error has occurred, no
	 * Finished Reading is going 
	 * to be received.
	 */
	public void onReadError(Exception ex) {
		notifyCallers(READING_ERROR,new HttpException(ex));
	}

	/**
	 * Network activity has been completed,
	 * ready to start parsing HTTP response.
	 */
	public void onFinishedReading() {
		
		// set status flag
		updateStatus(PARSING_DATA);

		// parse into JSON if need be
		if (_type == JSON_RESPONSE_TYPE) {
			try {
				final String js = new String(result.toByteArray());
				json = new JSONObject(js);
			}
			catch(JSONException ex){
				// record json parsing error
				json = null;
				
				// alert waiting callers
				notifyCallers(JSON_PARSING_ERROR,new HttpException(ex));
				
				// and run away
				return;
			}
		}
		// looking good
		updateStatus(DATA_READY);
	}
}
